package com.vois.jack.btmgr.devices.WLBleDfuDevice;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.SystemClock;
import android.util.Log;


import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;

import com.vois.jack.btmgr.blebase.BleAction;
import com.vois.jack.btmgr.blebase.BleDevice;
import com.vois.jack.btmgr.blebase.BleDeviceFsm;
import com.vois.jack.btmgr.classicbase.BtRecorderInterface;
import com.vois.jack.btmgr.common.IBtConstants;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.ArchiveInputStream;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.exception.DeviceDisconnectedException;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.exception.DfuException;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.exception.HexFileValidationException;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.exception.RemoteDfuException;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.exception.RemoteDfuExtendedErrorException;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.exception.UnknownResponseException;
import com.vois.jack.btmgr.devices.WLBleDfuDevice.internal.exception.UploadAbortedException;
import com.vois.jack.btmgr.util.Logger;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.UUID;
import java.util.zip.CRC32;


public class WLBleDfuDevice extends BleDevice implements DfuProgressInfo.ProgressListener {
    private static final String TAG = "WLBleDfuDevice";
    private Logger logger = Logger.getLogger(WLBleDfuDevice.class);
    static final public int BLE_DFU_PROGRESS_INFO_MESSAGE   = 0x10;

    // UUIDs used by the DFU
    private static final UUID DFU_SERVICE_UUID       = new UUID(0x0000FE5900001000L, 0x800000805F9B34FBL); // 16-bit UUID assigned by Bluetooth SIG
    private static final UUID DFU_CONTROL_POINT_UUID = new UUID(0x8EC90001F3154F60L, 0x9FB8838830DAEA50L);
    private static final UUID DFU_PACKET_UUID        = new UUID(0x8EC90002F3154F60L, 0x9FB8838830DAEA50L);

    private static final int DFU_STATUS_SUCCESS = 1;
    private static final int MAX_ATTEMPTS = 3;
    private static final int DEFAULT_PRN_VALUE = 12;
    private static final int DEFAULT_MBR_SIZE = 0x1000;

    // Object types
    private static final int OBJECT_COMMAND = 0x01;
    private static final int OBJECT_DATA = 0x02;
    // Operation codes and packets
    private static final int OP_CODE_CREATE_KEY = 0x01;
    private static final int OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY = 0x02;
    private static final int OP_CODE_CALCULATE_CHECKSUM_KEY = 0x03;
    private static final int OP_CODE_EXECUTE_KEY = 0x04;
    private static final int OP_CODE_SELECT_OBJECT_KEY = 0x06;
    private static final int OP_CODE_RESPONSE_CODE_KEY = 0x60;
    private static final byte[] OP_CODE_CREATE_COMMAND = new byte[]{OP_CODE_CREATE_KEY, OBJECT_COMMAND, 0x00, 0x00, 0x00, 0x00 };
    private static final byte[] OP_CODE_CREATE_DATA = new byte[]{OP_CODE_CREATE_KEY, OBJECT_DATA, 0x00, 0x00, 0x00, 0x00};
    private static final byte[] OP_CODE_PACKET_RECEIPT_NOTIF_REQ = new byte[]{OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY, 0x00, 0x00 /* param PRN uint16 in Little Endian */};
    private static final byte[] OP_CODE_CALCULATE_CHECKSUM = new byte[]{OP_CODE_CALCULATE_CHECKSUM_KEY};
    private static final byte[] OP_CODE_EXECUTE = new byte[]{OP_CODE_EXECUTE_KEY};
    private static final byte[] OP_CODE_SELECT_OBJECT = new byte[]{OP_CODE_SELECT_OBJECT_KEY, 0x00 /* type */};

    /**
     * Flag indicating whether the init packet has been already transferred or not.
     */
    private boolean mInitPacketInProgress;
    /**
     * Flag indicating whether the firmware is being transmitted or not.
     */
    private boolean mFirmwareUploadInProgress;
    /**
     * The number of packets of firmware data to be send before receiving a new Packets
     * receipt notification. 0 disables the packets notifications.
     */
    private int mPacketsBeforeNotification;
    /**
     * The number of packets sent since last notification.
     */
    private int mPacketsSentSinceNotification;
    /**
     * <p>
     * Flag set to <code>true</code> when the DFU target had send a notification with status other
     * than success. Setting it to <code>true</code> will abort sending firmware and
     * stop logging notifications (read below for explanation).
     * <p>
     * The onCharacteristicWrite(..) callback is called when Android writes the packet into the
     * outgoing queue, not when it physically sends the data. This means that the service will
     * first put up to N* packets, one by one, to the queue, while in fact the first one is transmitted.
     * In case the DFU target is in an invalid state it will notify Android with a notification
     * 10-03-02 for each packet of firmware that has been sent. After receiving the first such
     * notification, the DFU service will add the reset command to the outgoing queue,
     * but it will still be receiving such notifications until all the data packets are sent.
     * Those notifications should be ignored. This flag will prevent from logging
     * "Notification received..." more than once.
     * <p>
     * N* - Value of Packet Receipt Notification, 12 by default.
     */
    private boolean mRemoteErrorOccurred;
    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
    private static final int MAX_PACKET_SIZE_DEFAULT = 20; // the default maximum number of bytes in one packet is 20.

    /**
     * Lock used in synchronization purposes.
     */
    private final Object mLock = new Object();

    private InputStream mFirmwareStream;
    private InputStream mInitPacketStream;

    private boolean mConnected;
    /**
     * The firmware type. See TYPE_* constants.
     */
    private int mFileType;
    /**
     * Flag set to true if sending was aborted.
     */
    private boolean mAborted;
    /**
     * Flag indicating whether the request was completed or not
     */
    private boolean mRequestCompleted;
    /**
     * Flag sent when a request has been sent that will cause the DFU target to reset.
     * Often, after sending such command, Android throws a connection state error.
     * If this flag is set the error will be ignored.
     */
    private boolean mResetRequestSent;
    /**
     * The number of the last error that has occurred or 0 if there was no error.
     */
    private int mError;
    /**
     * Latest data received from device using notification.
     */
    private byte[] mReceivedData = null;
    private byte[] mBuffer = new byte[MAX_PACKET_SIZE_DEFAULT];
    private int mLastProgress = -1;
    private long mLastNotificationTime;
    private DfuProgressInfo mProgressInfo;
    private int mImageSizeInBytes;
    private int mInitPacketSizeInBytes;

    /**
     * The current connection state. If its value is > 0 than an error has occurred.
     * Error number is a negative value of mConnectionState
     */
    private int mConnectionState = STATE_CONNECTING;
    private final static int STATE_DISCONNECTED = 0;
    private final static int STATE_CONNECTING = -1;
    private final static int STATE_CONNECTED = -2;
    private final static int STATE_CONNECTED_AND_READY = -3; // indicates that services were discovered
    private final static int STATE_DISCONNECTING = -4;

    private DfuThread dfuThread;
    private int mRequestMtu = 160;
    private boolean controlPointEnable = false;
    private boolean inited = false;

    private class ObjectInfo extends ObjectChecksum {
        int maxSize;
    }

    private class ObjectChecksum {
        int offset;
        int CRC32;
    }


    class DfuThread extends Thread {
        @Override
        public void run() {
            logger.d("run: start running");
            // We have to wait until the device is connected and services are discovered
            // Connection error may occur as well.
            mError = 0;
            try {
                synchronized (mLock) {
                    while ((mConnectionState == STATE_CONNECTING || mConnectionState == STATE_CONNECTED) && mError == 0) {
                        mLock.wait();
                        logger.d("run: connect state:" + mConnectionState);
                    }
                }
            } catch (final InterruptedException e) {
                logger.d("Sleeping interrupted" + e.getMessage());
            }

            logger.d("run: mProgressInfo = null ? " + (mProgressInfo == null));

            mError = 0;
            synchronized (mLock) {
                try {
                    while (mConnected && !inited && !mAborted && mError == 0) {
                        mLock.wait();
                    }
                }catch (final InterruptedException e) {
                    e.printStackTrace();
                }
            }

            logger.d("run: after waiting for init:" + mConnected + " " + mAborted + " " + inited);

            if (!mConnected) {
                terminateConnection(0);
                return;
            }

            if (mAborted) {
                terminateConnection(0);
                return;
            }

            if (mError != 0) {
                terminateConnection(mError);
                return;
            }

            mProgressInfo.setProgress(DfuConstant.PROGRESS_CONNECTING);

            if (mAborted) {
                logger.d("Upload aborted");
                closeBleDevice();
                mProgressInfo.setProgress(DfuConstant.PROGRESS_ABORTED);
                return;
            }

            int size = 0;
            try {
                if (mInitPacketStream != null) {
                    if (mInitPacketStream.markSupported()) {
                        mInitPacketStream.reset();
                    }
                    size = mInitPacketStream.available();
                }
            } catch (final Exception e) {
                // ignore
            }

            mInitPacketSizeInBytes = size;

            try {
                if (mFirmwareStream.markSupported()) {
                    if (mFirmwareStream instanceof ArchiveInputStream) {
                        ((ArchiveInputStream) mFirmwareStream).fullReset();
                    } else {
                        mFirmwareStream.reset();
                    }
                }
                size = mFirmwareStream.available();
            } catch (final Exception e) {
                size = 0;
                // not possible
            }

            mImageSizeInBytes = size;
            mProgressInfo.init(size, 1, 1);

            mProgressInfo.setProgress(DfuConstant.PROGRESS_STARTING);

            logger.d("initialize: mImageSizeInBytes:" + mImageSizeInBytes);
            logger.d("initialize: mInitPacketSizeInBytes:" + mInitPacketSizeInBytes);
            logger.d("initialize: mFileType:" + mFileType);

            try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    requestMtu(mRequestMtu);
                }else {
                    setCurrentMTU(BleDevice.DEFAULT_MTU_SIZE);
                }

                logger.d("run: start enable cccd");
                enableCCCD();

                waitFor(1000);

                logger.d("run: send init packet");
                sendInitPacket();

                logger.d("run: send firmware");
                sendFirmware();

                logger.d("run: finish");
                mProgressInfo.setProgress(DfuConstant.PROGRESS_DISCONNECTING);
                waitUntilDisconnected();
                disconnectBleDevice();
                mProgressInfo.setProgress(DfuConstant.PROGRESS_COMPLETED);
                logger.d("run: finished!!!!!");

            }catch (final UploadAbortedException e) {
                logger.d("run: UploadAbortedException:" + e.getMessage());
                terminateConnection(0);
                mProgressInfo.setProgress(DfuConstant.PROGRESS_ABORTED);
            }catch (final DeviceDisconnectedException e) {
                logger.d("run: DeviceDisconnectedException:" + e.getMessage());
                e.printStackTrace();
            }catch (final DfuException e) {
                logger.d("run: DfuException:" + e.getMessage());
                terminateConnection(e.getErrorNumber());
            }catch (final UnknownResponseException e) {
                logger.d("run: UnknownResponseException:" + e.getMessage());
                terminateConnection(DfuConstant.ERROR_INVALID_RESPONSE);
            }catch (final RemoteDfuException e) {
                logger.d("run: RemoteDfuException:" + e.getMessage());
                final int error = DfuConstant.ERROR_REMOTE_TYPE_SECURE | e.getErrorNumber();

                if (e instanceof RemoteDfuExtendedErrorException) {
                    RemoteDfuExtendedErrorException ee = (RemoteDfuExtendedErrorException)e;
                    final int extendedError = DfuConstant.ERROR_REMOTE_TYPE_SECURE_EXTENDED | ee.getExtendedErrorNumber();
                    terminateConnection(extendedError | DfuConstant.ERROR_REMOTE_MASK);
                }else {
                    terminateConnection(error | DfuConstant.ERROR_REMOTE_MASK);
                }
            }
        }
    }

    @Override
    public boolean openBleDevice(boolean shouldStartWithScan) {
        boolean ret = super.openBleDevice(shouldStartWithScan);
        mConnectionState = STATE_CONNECTING;
        return ret;
    }

    @Override
    public boolean closeBleDevice() {
        boolean ret = super.closeBleDevice();
        abort();
        return ret;
    }

    private void waitUntilDisconnected() {
        try {
            synchronized (mLock) {
                while (mConnectionState != STATE_DISCONNECTED && mError == 0)
                    mLock.wait();
            }
        } catch (final InterruptedException e) {
            logger.d("Sleeping interrupted" + e.getMessage());
        }
    }

    private void writeOpCode(@NonNull final byte[] value, final boolean reset)
            throws DeviceDisconnectedException, DfuException, UploadAbortedException {
        if (mAborted)
            throw new UploadAbortedException();
        mReceivedData = null;
        mError = 0;
        mRequestCompleted = false;
        /*
         * Sending a command that will make the DFU target to reboot may cause an error 133
         * (0x85 - Gatt Error). If so, with this flag set, the error will not be shown to the user
         * as the peripheral is disconnected anyway.
         * See: mGattCallback#onCharacteristicWrite(...) method
         */
        mResetRequestSent = reset;

        writeCharacteristicReliable(DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, value, value.length, new BleAction.BleActionCallback() {
            @Override
            public void onActionResult(int status, Bundle result) {
                ParcelUuid parcelUuid = result.getParcelable(BleDeviceFsm.EXTRA_CHARACTERISTIC_UUID);
                BluetoothGattCharacteristic controlCharacteristic = getCharacteristic(DFU_SERVICE_UUID, parcelUuid.getUuid());
                onWriteResult(status, controlCharacteristic);
            }
        });

        // We have to wait for confirmation
        try {
            synchronized (mLock) {
                while (!mRequestCompleted && mConnected && mError == 0)
                    mLock.wait();
            }
        } catch (final InterruptedException e) {
            logger.d("Sleeping interrupted" + e.getMessage());
        }
        if (!mResetRequestSent && !mConnected)
            throw new DeviceDisconnectedException("Unable to write Op Code " + value[0] + ": device disconnected");
        if (!mResetRequestSent && mError != 0)
            throw new DfuException("Unable to write Op Code " + value[0], mError);
    }

    private byte[] readNotificationResponse()
            throws DeviceDisconnectedException, DfuException, UploadAbortedException {
        // do not clear the mReceiveData here. The response might already be obtained. Clear it in write request instead.
        try {
            synchronized (mLock) {
                while (mReceivedData == null && mConnected && mError == 0 && !mAborted)
                    mLock.wait(10000);
            }
        } catch (final InterruptedException e) {
            logger.d("Sleeping interrupted" + e.getMessage());
        }
        if (mAborted)
            throw new UploadAbortedException();
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to write Op Code: device disconnected");
        if (mError != 0)
            throw new DfuException("Unable to write Op Code", mError);
        return mReceivedData;
    }

    private void onWriteResult(int status, BluetoothGattCharacteristic characteristic) {
        logger.d("onWriteResult: status " + status + "UUID" + characteristic.getUuid().toString());
        if (status == BluetoothGatt.GATT_SUCCESS) {
            /*
             * This method is called when either a CONTROL POINT or PACKET characteristic has been written.
             * If it is the CONTROL POINT characteristic, just set the {@link mRequestCompleted}
             * flag to true. The main thread will continue its task when notified.
             * If the PACKET characteristic was written we must:
             * - if the image size was written in DFU Start procedure, just set flag to true
             * otherwise
             * - send the next packet, if notification is not required at that moment, or
             * - do nothing, because we have to wait for the notification to confirm the data received
             */
            if (characteristic.getUuid().equals(DFU_PACKET_UUID)) {
                logger.d("onWriteResult: enter packet branch");
                logger.d("onWriteResult: mInitPacketInProgress:" + mInitPacketInProgress);
                logger.d("onWriteResult: mFirmwareUploadInProgress:" + mFirmwareUploadInProgress);
                if (mInitPacketInProgress) {
                    // We've got confirmation that the init packet was sent
                    mInitPacketInProgress = false;
                } else if (mFirmwareUploadInProgress) {
                    // If the PACKET characteristic was written with image data, update counters
                    mProgressInfo.addBytesSent(characteristic.getValue().length);
                    mPacketsSentSinceNotification++;

                    final boolean notificationExpected = mPacketsBeforeNotification > 0 && mPacketsSentSinceNotification >= mPacketsBeforeNotification;
                    final boolean lastPacketTransferred = mProgressInfo.isComplete();
                    final boolean lastObjectPacketTransferred = mProgressInfo.isObjectComplete();

                    // When a Packet Receipt Notification notification is expected
                    // we must not call notifyLock() as the process will resume after notification is received.
                    if (notificationExpected)
                        return;

                    // In Secure DFU we (usually, depends on the page size and PRN value) do not get any notification after the object is completed,
                    // therefor the lock must be notified here to resume the main process.
                    if (lastPacketTransferred || lastObjectPacketTransferred) {
                        mFirmwareUploadInProgress = false;
                        notifyLock();
                        return;
                    }

                    // When neither of them is true, send the next packet
                    try {
                        // The writing might have been aborted (mAborted = true), an error might have occurred.
                        // In that case stop sending.
                        if (mAborted || mError != 0 || mRemoteErrorOccurred || mResetRequestSent) {
                            mFirmwareUploadInProgress = false;
                            notifyLock();
                            return;
                        }

                        final int available = mProgressInfo.getAvailableObjectSizeIsBytes();
                        byte[] buffer = mBuffer;
                        if (available < buffer.length)
                            buffer = new byte[available];
                        final int size = mFirmwareStream.read(buffer);
                        writePacket(characteristic, buffer, size);
                        return;
                    } catch (final HexFileValidationException e) {
                        logger.d("Invalid HEX file");
                        mError = DfuConstant.ERROR_FILE_INVALID;
                    } catch (final IOException e) {
                        logger.d("Error while reading the input stream" + e.getMessage());
                        mError = DfuConstant.ERROR_FILE_IO_EXCEPTION;
                    }
                }
            } else {
                logger.d("onWriteResult: in control branch");
                // If the CONTROL POINT characteristic was written just set the flag to true.
                // The main thread will continue its task when notified.
                mRequestCompleted = true;
            }
        } else {
            /*
             * If a Reset (Op Code = 6) or Activate and Reset (Op Code = 5) commands are sent,
             * the DFU target resets and sometimes does it so quickly that does not manage to send
             * any ACK to the controller and error 133 is thrown here. This bug should be fixed
             * in SDK 8.0+ where the target would gracefully disconnect before restarting.
             */
            if (mResetRequestSent)
                mRequestCompleted = true;
            else {
                logger.d("Characteristic write error: " + status);
                mError = DfuConstant.ERROR_CONNECTION_MASK | status;
            }
        }
        notifyLock();
    }

    private int getStatusCode(final byte[] response, final int request) throws UnknownResponseException {
        if (response == null || response.length < 3 || response[0] != OP_CODE_RESPONSE_CODE_KEY || response[1] != request ||
                (response[2] != DFU_STATUS_SUCCESS &&
                        response[2] != SecureDfuError.OP_CODE_NOT_SUPPORTED &&
                        response[2] != SecureDfuError.INVALID_PARAM &&
                        response[2] != SecureDfuError.INSUFFICIENT_RESOURCES &&
                        response[2] != SecureDfuError.INVALID_OBJECT &&
                        response[2] != SecureDfuError.UNSUPPORTED_TYPE &&
                        response[2] != SecureDfuError.OPERATION_NOT_PERMITTED &&
                        response[2] != SecureDfuError.OPERATION_FAILED &&
                        response[2] != SecureDfuError.EXTENDED_ERROR)) {
            logger.d("getStatusCode: error:" + response[2]);
            throw new UnknownResponseException("Invalid response received", response, OP_CODE_RESPONSE_CODE_KEY, request);
        }
        return response[2];
    }

    private ObjectInfo selectObject(final int type)
            throws DeviceDisconnectedException, DfuException, UploadAbortedException,
            RemoteDfuException, UnknownResponseException {
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to read object info: device disconnected");

        OP_CODE_SELECT_OBJECT[1] = (byte) type;
        writeOpCode(OP_CODE_SELECT_OBJECT, false);

        final byte[] response = readNotificationResponse();
        final int status = getStatusCode(response, OP_CODE_SELECT_OBJECT_KEY);
        if (status == SecureDfuError.EXTENDED_ERROR)
            throw new RemoteDfuExtendedErrorException("Selecting object failed", response[3]);
        if (status != DFU_STATUS_SUCCESS)
            throw new RemoteDfuException("Selecting object failed", status);

        final ObjectInfo info = new ObjectInfo();
        BluetoothGattCharacteristic characteristic = getCharacteristic(DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID);
        info.maxSize = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3);
        info.offset = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3 + 4);
        info.CRC32 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3 + 8);
        return info;
    }

    private void setNumberOfPackets(@SuppressWarnings("SameParameterValue") @NonNull final byte[] data, final int value) {
        data[1] = (byte) (value & 0xFF);
        data[2] = (byte) ((value >> 8) & 0xFF);
    }

    private void setPacketReceiptNotifications(final int number)
            throws DfuException, DeviceDisconnectedException, UploadAbortedException,
            UnknownResponseException, RemoteDfuException {
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to read Checksum: device disconnected");

        // Send the number of packets of firmware before receiving a receipt notification
        logger.d("Sending the number of packets before notifications (Op Code = 2, Value = " + number + ")");
        setNumberOfPackets(OP_CODE_PACKET_RECEIPT_NOTIF_REQ, number);
        writeOpCode(OP_CODE_PACKET_RECEIPT_NOTIF_REQ, false);

        // Read response
        final byte[] response = readNotificationResponse();
        final int status = getStatusCode(response, OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY);
        if (status == SecureDfuError.EXTENDED_ERROR)
            throw new RemoteDfuExtendedErrorException("Sending the number of packets failed", response[3]);
        if (status != DFU_STATUS_SUCCESS)
            throw new RemoteDfuException("Sending the number of packets failed", status);
    }

    private void setObjectSize(@NonNull final byte[] data, final int value) {
        data[2] = (byte) (value & 0xFF);
        data[3] = (byte) ((value >> 8) & 0xFF);
        data[4] = (byte) ((value >> 16) & 0xFF);
        data[5] = (byte) ((value >> 24) & 0xFF);
    }

    private void writeCreateRequest(final int type, final int size)
            throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException,
            UnknownResponseException {
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to create object: device disconnected");

        final byte[] data = (type == OBJECT_COMMAND) ? OP_CODE_CREATE_COMMAND : OP_CODE_CREATE_DATA;
        setObjectSize(data, size);
        writeOpCode(data, false);

        final byte[] response = readNotificationResponse();
        final int status = getStatusCode(response, OP_CODE_CREATE_KEY);
        if (status == SecureDfuError.EXTENDED_ERROR)
            throw new RemoteDfuExtendedErrorException("Creating Command object failed", response[3]);
        if (status != DFU_STATUS_SUCCESS)
            throw new RemoteDfuException("Creating Command object failed", status);
    }

    private void writeInitPacket(final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size)
            throws DeviceDisconnectedException, DfuException, UploadAbortedException {
        if (mAborted)
            throw new UploadAbortedException();
        byte[] locBuffer = buffer;
        if (buffer.length != size) {
            locBuffer = new byte[size];
            System.arraycopy(buffer, 0, locBuffer, 0, size);
        }
        mReceivedData = null;
        mError = 0;
        mInitPacketInProgress = true;

        logger.d("writeInitPacket: UUID:" + characteristic.getUuid().toString());
        logger.d("Sending init packet (Value = " + parse(locBuffer) + ")");
        writeCharacteristicData(DFU_SERVICE_UUID, characteristic.getUuid(), locBuffer, locBuffer.length, new BleAction.BleActionCallback() {
            @Override
            public void onActionResult(int status, Bundle result) {
                ParcelUuid parcelUuid = result.getParcelable(BleDeviceFsm.EXTRA_CHARACTERISTIC_UUID);
                BluetoothGattCharacteristic packetCharacteristic = getCharacteristic(DFU_SERVICE_UUID, parcelUuid.getUuid());
                onWriteResult(status, packetCharacteristic);
            }
        });

        logger.d("writeInitPacket: mInitPacketInProgress:" + mInitPacketInProgress);

        // We have to wait for confirmation
        try {
            synchronized (mLock) {
                while (mInitPacketInProgress && mConnected && mError == 0)
                    mLock.wait();
            }
        } catch (final InterruptedException e) {
            logger.d("Sleeping interrupted" + e.getMessage());
        }

        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to write Init DFU Parameters: device disconnected");
        if (mError != 0)
            throw new DfuException("Unable to write Init DFU Parameters", mError);
    }


    private void writeInitData(final BluetoothGattCharacteristic characteristic, final CRC32 crc32)
            throws DfuException, DeviceDisconnectedException, UploadAbortedException {
        try {
            byte[] data = mBuffer;
            int size;
            while ((size = mInitPacketStream.read(data, 0, data.length)) != -1) {
                writeInitPacket(characteristic, data, size);
                if (crc32 != null)
                    crc32.update(data, 0, size);
            }
        } catch (final IOException e) {
            logger.d("Error while reading Init packet file" + e.getMessage());
            throw new DfuException("Error while reading Init packet file", DfuConstant.ERROR_FILE_ERROR);
        }
    }

    private ObjectChecksum readChecksum() throws DeviceDisconnectedException, DfuException,
            UploadAbortedException, RemoteDfuException, UnknownResponseException {
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to read Checksum: device disconnected");

        writeOpCode(OP_CODE_CALCULATE_CHECKSUM, false);

        final byte[] response = readNotificationResponse();
        final int status = getStatusCode(response, OP_CODE_CALCULATE_CHECKSUM_KEY);
        if (status == SecureDfuError.EXTENDED_ERROR)
            throw new RemoteDfuExtendedErrorException("Receiving Checksum failed", response[3]);
        if (status != DFU_STATUS_SUCCESS)
            throw new RemoteDfuException("Receiving Checksum failed", status);

        final ObjectChecksum checksum = new ObjectChecksum();
        BluetoothGattCharacteristic characteristic = getCharacteristic(DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID);
        checksum.offset = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3);
        checksum.CRC32 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3 + 4);
        return checksum;
    }

    private void terminateConnection(int error) {
        Message message = Message.obtain();
        message.arg1 = error;
        message.what = DfuMessage.DFU_ERROR_INFO_MESSAGE.getValue();

        if (getListener() != null) {
            getListener().onMessage(getBluetoothDevice(), message);
        }
    }

    private void sendInitPacket()
            throws RemoteDfuException, DeviceDisconnectedException, DfuException, UploadAbortedException, UnknownResponseException {
        final CRC32 crc32 = new CRC32(); // Used to calculate CRC32 of the Init packet
        ObjectChecksum checksum;

        // First, select the Command Object. As a response the maximum command size and information whether there is already
        // a command saved from a previous connection is returned.
        logger.d("Setting object to Command (Op Code = 6, Type = 1)");
        final ObjectInfo info = selectObject(OBJECT_COMMAND);
        logger.d(String.format(Locale.US, "Command object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));

        //noinspection StatementWithEmptyBody
        if (mInitPacketSizeInBytes > info.maxSize) {
            // Ignore this here. Later, after sending the 'Create object' command, DFU target will send an error if init packet is too large
        }

        // The Init packet is sent different way in this implementation than the firmware, and receiving PRNs is not implemented.
        // This value might have been stored on the device, so we have to explicitly disable PRNs.
        setPacketReceiptNotifications(0);

        for (int attempt = 1; attempt <= MAX_ATTEMPTS;) {
            logger.d("Creating Init packet object (Op Code = 1, Type = 1, Size = " + mInitPacketSizeInBytes + ")");
            writeCreateRequest(OBJECT_COMMAND, mInitPacketSizeInBytes);

            // Write Init data to the Packet Characteristic
            try {
                logger.d("Sending " + (mInitPacketSizeInBytes - info.offset) + " bytes of init packet...");
                writeInitData(getCharacteristic(DFU_SERVICE_UUID, DFU_PACKET_UUID), crc32);
            } catch (final DeviceDisconnectedException e) {
                logger.d("Disconnected while sending init packet");
                throw e;
            }
            final int crc = (int) (crc32.getValue() & 0xFFFFFFFFL);

            // Calculate Checksum
            logger.d("Sending Calculate Checksum command (Op Code = 3)");
            checksum = readChecksum();
            logger.d(String.format(Locale.US, "Checksum received (Offset = %d, CRC = %08X)", checksum.offset, checksum.CRC32));
            logger.d(String.format(Locale.US, "crc checksum:%08X CRC = %08X", crc, checksum.CRC32));
            if (crc == checksum.CRC32) {
                // Everything is OK, we can proceed
                break;
            } else {
                if (attempt < MAX_ATTEMPTS) {
                    attempt++;
                    logger.d("CRC does not match! Retrying...(" + attempt + "/" + MAX_ATTEMPTS + ")");
                    try {
                        // Go back to the beginning, we will send the whole Init packet again
                        info.offset = 0;
                        info.CRC32 = 0;
                        mInitPacketStream.reset();
                        crc32.reset();
                    } catch (final IOException e) {
                        logger.d("Error while resetting the init packet stream" + e.getMessage());
                        terminateConnection(DfuConstant.ERROR_FILE_IO_EXCEPTION);
                        return;
                    }
                } else {
                    logger.d("CRC does not match!");
                    terminateConnection(DfuConstant.ERROR_CRC_ERROR);
                    return;
                }
            }
        }

        // Execute Init packet. It's better to execute it twice than not execute at all...
        logger.d("Executing init packet (Op Code = 4)");
        writeExecute();
    }

    private void writeExecute() throws DfuException, DeviceDisconnectedException,
            UploadAbortedException, UnknownResponseException, RemoteDfuException {
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to read Checksum: device disconnected");

        writeOpCode(OP_CODE_EXECUTE, false);

        final byte[] response = readNotificationResponse();
        final int status = getStatusCode(response, OP_CODE_EXECUTE_KEY);
        logger.d("writeExecute: status:" + status);
        if (status == SecureDfuError.EXTENDED_ERROR)
            throw new RemoteDfuExtendedErrorException("Executing object failed", response[3]);
        if (status != DFU_STATUS_SUCCESS)
            throw new RemoteDfuException("Executing object failed", status);
    }

    private void waitFor(final int millis) {
        synchronized (mLock) {
            try {
                mLock.wait(millis);
            } catch (final InterruptedException e) {
                logger.d("Sleeping interrupted" + e.getMessage());
            }
        }
    }

    private void notifyLock() {
        // Notify waiting thread
        synchronized (mLock) {
            mLock.notifyAll();
        }
    }

    private void enableCCCD()
            throws DeviceDisconnectedException, DfuException, UploadAbortedException {
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to set Notifiaction state: device disconnected");
        if (mAborted)
            throw new UploadAbortedException();

        mReceivedData = null;
        mError = 0;
        mRequestCompleted = false;
        controlPointEnable = false;

        BleAction bleAction = new BleAction();
        Bundle bundle = new Bundle();
        bleAction.setCmd(BleAction.BleCmd.BLE_CMD_ENABLE_NOTIFICATION);
        bundle.putParcelable(BleDeviceFsm.EXTRA_SERVICE_UUID, new ParcelUuid(DFU_SERVICE_UUID));
        bundle.putParcelable(BleDeviceFsm.EXTRA_CHARACTERISTIC_UUID, new ParcelUuid(DFU_CONTROL_POINT_UUID));
        bundle.putBoolean(BleDeviceFsm.EXTRA_ENABLE_VALUE, true);
        bleAction.setCmdArg(bundle);
        bleAction.setCallback(new BleAction.BleActionCallback() {
            @Override
            public void onActionResult(int status, Bundle result) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    mRequestCompleted = true;
                    controlPointEnable = true;
                }else {
                    mError = DfuConstant.ERROR_CONNECTION_MASK | status;
                }

                logger.d("onActionResult: enable ccccd status:" + status);
                notifyLock();
            }
        });

        if (getBleDeviceFsm() != null) {
            getBleDeviceFsm().executeBleAction(bleAction);
        }

        // We have to wait until device receives a response or an error occur
        try {
            synchronized (mLock) {
                while (!controlPointEnable && mConnected && mError == 0) {
                    mLock.wait();
                    // Check the value of the CCCD
                }
            }
        } catch (final InterruptedException e) {
            logger.d("Sleeping interrupted" + e.getMessage());
        }

        logger.d("enableCCCD: finish enableCCCD");

        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to set notification state: device disconnected");
        if (mError != 0)
            throw new DfuException("Unable to set notification state", mError);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void requestMtu(@IntRange(from = 0, to = 517) final int mtu)
            throws DeviceDisconnectedException, UploadAbortedException {
        if (mAborted)
            throw new UploadAbortedException();
        mRequestCompleted = false;

        negotiateMTU(mRequestMtu, new BleAction.BleActionCallback() {
            @Override
            public void onActionResult(int status, Bundle result) {
                if (status == BluetoothGatt.GATT_SUCCESS) {

                    if (mtu - 3 > mBuffer.length)
                        mBuffer = new byte[mtu - 3]; // Maximum payload size is MTU - 3 bytes
                    setCurrentMTU(mtu);
                    logger.d("MTU changed to: " + mtu);
                } else {
                    logger.d("Changing MTU failed: " + status + " (mtu: " + mtu + ")");
                    if (status == 4 /* Invalid PDU */) {
                        mBuffer = new byte[20]; // Maximum payload size is MTU - 3 bytes
                    }
                }
                mRequestCompleted = true;
                notifyLock();
            }
        });

        // We have to wait until the MTU exchange finishes
        try {
            synchronized (mLock) {
                while (!mRequestCompleted && mConnected && mError == 0)
                    mLock.wait();
            }
        } catch (final InterruptedException e) {
            logger.d("Sleeping interrupted" + e.getMessage());
        }

        logger.d("requestMtu: mtu is get already");
        if (!mConnected)
            throw new DeviceDisconnectedException("Unable to read Service Changed CCCD: device disconnected");
    }

    public WLBleDfuDevice() {
        super();

        mConnectionState = STATE_DISCONNECTED;
        mError = 0;
        mFileType = DfuConstant.TYPE_AUTO;
    }

    public boolean init(String filePath, int requestMtu) {
        mRequestMtu = requestMtu;
        mPacketsBeforeNotification = DEFAULT_PRN_VALUE;

        try {
            mError = 0;
            final InputStream is = new FileInputStream(filePath);
            mFirmwareStream = new ArchiveInputStream(is, DEFAULT_MBR_SIZE, mFileType);

            final int imageSizeInBytes = mFirmwareStream.available();
            if ((imageSizeInBytes % 4) != 0) {
                logger.d("init: image size in byte is not 4 times");
                return false;
            }

            // Update the file type bit field basing on the ZIP content
            final ArchiveInputStream zhis = (ArchiveInputStream) mFirmwareStream;
            int fileType;
            fileType = zhis.getContentType();

            // Validate sizes
            if ((fileType & DfuConstant.TYPE_APPLICATION) > 0 && (zhis.applicationImageSize() % 4) != 0)
                return false;

            if (fileType == DfuConstant.TYPE_APPLICATION) {
                if (zhis.getApplicationInit() != null)
                    mInitPacketStream = new ByteArrayInputStream(zhis.getApplicationInit());
            } else {
                if (zhis.getSystemInit() != null)
                    mInitPacketStream = new ByteArrayInputStream(zhis.getSystemInit());
            }

            mFirmwareStream.mark(mFirmwareStream.available());
            if (mInitPacketStream != null)
                mInitPacketStream.mark(mInitPacketStream.available());

            mProgressInfo = new DfuProgressInfo(this);
            logger.d("init: create progress info");

            inited = true;
            notifyLock();
            logger.d("init: notify Lock");

        }catch (IOException e) {
            e.printStackTrace();
            mError = DfuConstant.ERROR_FILE_ERROR;
            notifyLock();
            return false;
        }

        return true;
    }

    private void writeExecute(final boolean allowRetry)
            throws DfuException, DeviceDisconnectedException, UploadAbortedException,
            UnknownResponseException, RemoteDfuException {
        try {
            writeExecute();
        } catch (final RemoteDfuException e) {
            if (allowRetry && e.getErrorNumber() == SecureDfuError.INVALID_OBJECT) {
                logger.d(e.getMessage() + ": " + SecureDfuError.parse(DfuConstant.ERROR_REMOTE_TYPE_SECURE | SecureDfuError.INVALID_OBJECT));
                Log.d(TAG,"SD busy? Retrying...");
                Log.d(TAG,"Executing data object (Op Code = 4)");
                writeExecute();
            } else {
                throw e;
            }
        }
    }

    private void uploadFirmwareImage(final BluetoothGattCharacteristic packetCharacteristic)
            throws DeviceDisconnectedException, DfuException, UploadAbortedException {
        if (mAborted)
            throw new UploadAbortedException();
        mReceivedData = null;
        mError = 0;
        mFirmwareUploadInProgress = true;
        mPacketsSentSinceNotification = 0;

        final byte[] buffer = mBuffer;
        try {
            final int size = mFirmwareStream.read(buffer);
            writePacket(packetCharacteristic, buffer, size);
        } catch (final HexFileValidationException e) {
            throw new DfuException("HEX file not valid", DfuConstant.ERROR_FILE_INVALID);
        } catch (final IOException e) {
            throw new DfuException("Error while reading file", DfuConstant.ERROR_FILE_IO_EXCEPTION);
        }

        try {
            synchronized (mLock) {
                while (mFirmwareUploadInProgress && mReceivedData == null && mConnected && mError == 0)
                    mLock.wait();
            }
        } catch (final InterruptedException e) {
            logger.d("Sleeping interrupted" + e.getMessage());
        }

        if (!mConnected)
            throw new DeviceDisconnectedException("Uploading Firmware Image failed: device disconnected");
        if (mError != 0)
            throw new DfuException("Uploading Firmware Image failed", mError);
    }


    private void sendFirmware() throws RemoteDfuException,
            DeviceDisconnectedException, DfuException, UploadAbortedException, UnknownResponseException {
        // Send the number of packets of firmware before receiving a receipt notification
        int numberOfPacketsBeforeNotification = mPacketsBeforeNotification;
        if (numberOfPacketsBeforeNotification > 0) {
            setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
        }

        // We are ready to start sending the new firmware.

        logger.d("Setting object to Data (Op Code = 6, Type = 2)");
        final ObjectInfo info = selectObject(OBJECT_DATA);
        logger.d(String.format(Locale.US,
                "Data object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
        mProgressInfo.setMaxObjectSizeInBytes(info.maxSize);

        // Number of chunks in which the data will be sent
        final int chunkCount = (mImageSizeInBytes + info.maxSize - 1) / info.maxSize;
        int currentChunk = 0;
        boolean resumeSendingData = false;

        // Can we resume? If the offset obtained from the device is greater then zero we can compare it with the local CRC
        // and resume sending the data.
        if (info.offset > 0) {
            try {
                currentChunk = info.offset / info.maxSize;
                int bytesSentAndExecuted = info.maxSize * currentChunk;
                int bytesSentNotExecuted = info.offset - bytesSentAndExecuted;

                // If the offset is dividable by maxSize, assume that the last page was not executed
                if (bytesSentNotExecuted == 0) {
                    bytesSentAndExecuted -= info.maxSize;
                    bytesSentNotExecuted = info.maxSize;
                }

                // Read the same number of bytes from the current init packet to calculate local CRC32
                if (bytesSentAndExecuted > 0) {
                    mFirmwareStream.read(new byte[bytesSentAndExecuted]); // Read executed bytes
                    mFirmwareStream.mark(info.maxSize); // Mark here
                }
                // Here the bytesSentNotExecuted is for sure greater then 0
                mFirmwareStream.read(new byte[bytesSentNotExecuted]); // Read the rest

                // Calculate the CRC32
                final int crc = (int) (((ArchiveInputStream) mFirmwareStream).getCrc32() & 0xFFFFFFFFL);

                if (crc == info.CRC32) {
                    logger.d(info.offset + " bytes of data sent before, CRC match");
                    mProgressInfo.setBytesSent(info.offset);
                    mProgressInfo.setBytesReceived(info.offset);

                    // If the whole page was sent and CRC match, we have to make sure it was executed
                    if (bytesSentNotExecuted == info.maxSize && info.offset < mImageSizeInBytes) {
                        logger.d("Executing data object (Op Code = 4)");
                        writeExecute();
                    } else {
                        resumeSendingData = true;
                    }
                } else {
                    logger.d(info.offset + " bytes sent before, CRC does not match");
                    // The CRC of the current object is not correct. If there was another Data object sent before, its CRC must have been correct,
                    // as it has been executed. Either way, we have to create the current object again.
                    mProgressInfo.setBytesSent(bytesSentAndExecuted);
                    mProgressInfo.setBytesReceived(bytesSentAndExecuted);
                    info.offset -= bytesSentNotExecuted;
                    info.CRC32 = 0; // invalidate
                    mFirmwareStream.reset();
                    logger.d("Resuming from byte " + info.offset + "...");
                }
            } catch (final IOException e) {
                logger.d("Error while reading firmware stream" + e.getMessage());
                terminateConnection(DfuConstant.ERROR_FILE_IO_EXCEPTION);
                return;
            }
        } else {
            // Initialize the timer used to calculate the transfer speed
            mProgressInfo.setBytesSent(0);
        }

        final long startTime = SystemClock.elapsedRealtime();

        if (info.offset < mImageSizeInBytes) {
            int attempt = 1;
            // Each page will be sent in MAX_ATTEMPTS
            while (mProgressInfo.getAvailableObjectSizeIsBytes() > 0) {
                if (!resumeSendingData) {
                    // Create the Data object
                    final int availableObjectSizeInBytes = mProgressInfo.getAvailableObjectSizeIsBytes();
                    logger.d("Creating Data object (Op Code = 1, Type = 2, Size = " + availableObjectSizeInBytes + ") (" + (currentChunk + 1) + "/" + chunkCount + ")");
                    writeCreateRequest(OBJECT_DATA, availableObjectSizeInBytes);
                } else {
                    resumeSendingData = false;
                }

                // Send the current object part
                try {
                    logger.d("Uploading firmware...");
                    uploadFirmwareImage(getCharacteristic(DFU_SERVICE_UUID, DFU_PACKET_UUID));
                } catch (final DeviceDisconnectedException e) {
                    logger.d("Disconnected while sending data");
                    throw e;
                }

                // Calculate Checksum
                logger.d("Sending Calculate Checksum command (Op Code = 3)");
                final ObjectChecksum checksum = readChecksum();
                logger.d(String.format(Locale.US, "Checksum received (Offset = %d, CRC = %08X)", checksum.offset, checksum.CRC32));

                // It may happen, that not all bytes that were sent were received by the remote device
                final int bytesLost = mProgressInfo.getBytesSent() - checksum.offset;
                if (bytesLost > 0) {
                    logger.d(bytesLost + " bytes were lost!");

                    try {
                        // We have to reset the stream and read 'offset' number of bytes to recalculate the CRC
                        mFirmwareStream.reset(); // Resets to the beginning of current object
                        mFirmwareStream.read(new byte[info.maxSize - bytesLost]); // Reads additional bytes that were sent and received in this object
                        mProgressInfo.setBytesSent(checksum.offset);
                    } catch (final IOException e) {
                        logger.d("Error while reading firmware stream", e);
                        terminateConnection(DfuConstant.ERROR_FILE_IO_EXCEPTION);
                        return;
                    }
                    // To decrease the chance of loosing data next time let's set PRN to 1. This will make the update very long, but perhaps it will succeed.
                    numberOfPacketsBeforeNotification = mPacketsBeforeNotification = 1;
                    setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
                }

                // Calculate the CRC32
                final int crc = (int) (((ArchiveInputStream) mFirmwareStream).getCrc32() & 0xFFFFFFFFL);
                if (crc == checksum.CRC32) {
                    if (bytesLost > 0) {
                        resumeSendingData = true;
                        continue;
                    }
                    // Execute Init packet
                    logger.d("Executing data object (Op Code = 4)");
                    writeExecute(mProgressInfo.isComplete());

                    // Increment iterator
                    currentChunk++;
                    attempt = 1;
                    //Mark this location after completion of successful transfer.  In the event of a CRC retry on the next packet we will restart from this point.
                    mFirmwareStream.mark(0);
                } else {
                    String crcFailMessage = String.format(Locale.US, "CRC does not match! Expected %08X but found %08X.", crc, checksum.CRC32);
                    if (attempt < MAX_ATTEMPTS) {
                        attempt++;
                        crcFailMessage += String.format(Locale.US, " Retrying...(%d/%d)", attempt, MAX_ATTEMPTS);
                        logger.d(crcFailMessage);
                        try {
                            // Reset the CRC and file pointer back to the previous mark() point after completion of the last successful packet.
                            mFirmwareStream.reset();
                            mProgressInfo.setBytesSent(((ArchiveInputStream) mFirmwareStream).getBytesRead());
                        } catch (final IOException e) {
                            logger.d("Error while resetting the firmware stream" + e.getMessage());
                            terminateConnection(DfuConstant.ERROR_FILE_IO_EXCEPTION);
                            return;
                        }
                    } else {
                        logger.d(crcFailMessage);
                        terminateConnection(DfuConstant.ERROR_CRC_ERROR);
                        return;
                    }
                }
            }
        } else {
            // Looks as if the whole file was sent correctly but has not been executed
            logger.d("Executing data object (Op Code = 4)");
            writeExecute(true);
        }

        final long endTime = SystemClock.elapsedRealtime();
        logger.d("Transfer of " + (mProgressInfo.getBytesSent() - info.offset) + " bytes has taken " + (endTime - startTime) + " ms");
    }

    @Override
    public void onConnectStateChanged(int state, int reason) {
        super.onConnectStateChanged(state, reason);

        if (state == BluetoothGatt.STATE_CONNECTED) {
            mConnectionState = STATE_CONNECTED;
            mConnected = true;
        }else if (state == BluetoothGatt.STATE_CONNECTING) {
            mConnectionState = STATE_CONNECTING;
        }else if (state == BluetoothGatt.STATE_DISCONNECTING) {
            mConnectionState = STATE_DISCONNECTING;
        }else if (state == BluetoothGatt.STATE_DISCONNECTED) {
            mConnectionState = STATE_DISCONNECTED;
            mConnected = false;
        }

        notifyLock();
    }

    @Override
    public void getValidationData() {

    }

    @Override
    public String getDeviceModel() {
        return null;
    }

    @Override
    public void setSeqID(String seqID) {

    }

    @Override
    public void getVendor() {

    }

    @Override
    public void getVersion() {

    }

    @Override
    public void onScanStateChanged(int state) {

    }

    @Override
    public void onVerifying() {

    }

    @Override
    public String getProtocolName() {
        return IBtConstants.PROTOCOL_VOIS;
    }

    private String parse(final byte[] data) {
        if (data == null)
            return "";
        final int length = data.length;
        if (length == 0)
            return "";

        final char[] out = new char[length * 3 - 1];
        for (int j = 0; j < length; j++) {
            int v = data[j] & 0xFF;
            out[j * 3] = HEX_ARRAY[v >>> 4];
            out[j * 3 + 1] = HEX_ARRAY[v & 0x0F];
            if (j != length - 1)
                out[j * 3 + 2] = '-';
        }
        return new String(out);
    }

    private void writePacket(final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) {
        byte[] locBuffer = buffer;
        if (size <= 0) // This should never happen
            return;
        if (buffer.length != size) {
            locBuffer = new byte[size];
            System.arraycopy(buffer, 0, locBuffer, 0, size);
        }

        writeCharacteristicData(DFU_SERVICE_UUID, characteristic.getUuid(), locBuffer, locBuffer.length, new BleAction.BleActionCallback() {
            @Override
            public void onActionResult(int status, Bundle result) {
                ParcelUuid parcelUuid = result.getParcelable(BleDeviceFsm.EXTRA_CHARACTERISTIC_UUID);
                BluetoothGattCharacteristic packetCharacteristic = getCharacteristic(DFU_SERVICE_UUID, parcelUuid.getUuid());
                onWriteResult(status, packetCharacteristic);
            }
        });
    }

    private void handlePacketReceiptNotification(final BluetoothGattCharacteristic characteristic) {
        // Secure DFU:
        // When PRN is set to be received after the object is complete we don't want to send anything. First the object needs to be executed.
        if (!mFirmwareUploadInProgress) {
            handleNotification(characteristic);
            return;
        }

        final BluetoothGattCharacteristic packetCharacteristic =
                getCharacteristic(DFU_SERVICE_UUID, DFU_PACKET_UUID);
        try {
            mPacketsSentSinceNotification = 0;

            // The writing might have been aborted (mAborted = true), an error might have occurred.
            // In that case quit sending.
            if (mAborted || mError != 0 || mRemoteErrorOccurred || mResetRequestSent) {
                mFirmwareUploadInProgress = false;
                return;
            }

            final boolean lastPacketTransferred = mProgressInfo.isComplete();
            final boolean lastObjectPacketTransferred = mProgressInfo.isObjectComplete();

            if (lastPacketTransferred || lastObjectPacketTransferred) {
                mFirmwareUploadInProgress = false;
                notifyLock();
                return;
            }

            final int available = mProgressInfo.getAvailableObjectSizeIsBytes();
            byte[] buffer = mBuffer;
            if (available < buffer.length)
                buffer = new byte[available];
            final int size = mFirmwareStream.read(buffer);
            writePacket(packetCharacteristic, buffer, size);
        } catch (final HexFileValidationException e) {
            logger.d("Invalid HEX file");
            mError = DfuConstant.ERROR_FILE_INVALID;
        } catch (final IOException e) {
            logger.d("Error while reading the input stream" + e.getMessage());
            mError = DfuConstant.ERROR_FILE_IO_EXCEPTION;
        }
    }

    private void handleNotification(final BluetoothGattCharacteristic characteristic) {
        mReceivedData = characteristic.getValue();
        mFirmwareUploadInProgress = false;
    }

    @Override
    public void onReady() {
        super.onReady();

        mConnectionState = STATE_CONNECTED_AND_READY;

        logger.d("onReady: ");

        notifyLock();
    }

    @Override
    public void onCharacteristiChanged(UUID serviceUUID, UUID characteristicUUID, byte[] data) {
        BluetoothGattCharacteristic characteristic = getCharacteristic(serviceUUID, characteristicUUID);

        if (characteristic.getValue() == null || characteristic.getValue().length < 3) {
            logger.d("Empty response: " + parse(characteristic.getValue()));
            mError = DfuConstant.ERROR_INVALID_RESPONSE;
            notifyLock();
            return;
        }

        final int responseType = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
        logger.d("onCharacteristiChanged: responseType:" + responseType);

        // The first byte should always be the response code
        if (responseType == OP_CODE_RESPONSE_CODE_KEY) {
            final int requestType = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1);
            logger.d("onCharacteristiChanged: requestType:" + requestType);

            switch (requestType) {
                case OP_CODE_CALCULATE_CHECKSUM_KEY: {
                    final int offset = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3);
                    final int remoteCrc = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 3 + 4);
                    final int localCrc = (int) (((ArchiveInputStream) mFirmwareStream).getCrc32() & 0xFFFFFFFFL);
                    // Check whether local and remote CRC match
                    if (localCrc == remoteCrc) {
                        // If so, update the number of bytes received
                        mProgressInfo.setBytesReceived(offset);
                    } else {
                        // Else, and only in case it was a PRN received, not the response for
                        // Calculate Checksum Request, stop sending data
                        if (mFirmwareUploadInProgress) {
                            mFirmwareUploadInProgress = false;
                            notifyLock();
                            return;
                        } // else will be handled by sendFirmware(gatt) below
                    }
                    handlePacketReceiptNotification(characteristic);
                    break;
                }
                default: {
                    /*
                     * If the DFU target device is in invalid state (e.g. the Init Packet is
                     * required but has not been selected), the target will send
                     * DFU_STATUS_INVALID_STATE error for each firmware packet that was send.
                     * We are interested may ignore all but the first one.
                     */
                    if (mRemoteErrorOccurred)
                        break;
                    final int status = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 2);
                    logger.d("onCharacteristiChanged: status:" + status);
                    if (status != DFU_STATUS_SUCCESS)
                        mRemoteErrorOccurred = true;

                    handleNotification(characteristic);
                    break;
                }
            }
        } else {
            logger.d("Invalid response: " + parse(characteristic.getValue()));
            mError = DfuConstant.ERROR_INVALID_RESPONSE;
        }
        notifyLock();

    }

    @Override
    public void onReliableWriteCompleted(int status) {

    }

    @Override
    public void updateProgressNotification() {
        final DfuProgressInfo info = mProgressInfo;
        final int progress = info.getProgress();
        if (mLastProgress == progress)
            return;

        mLastProgress = progress;

        // the notification may not be refreshed too quickly as the ABORT button becomes not clickable
        // If new state is an end-state, update regardless so it will not stick around in "Disconnecting" state
        final long now = SystemClock.elapsedRealtime();
        if (now - mLastNotificationTime < 250 && !(DfuConstant.PROGRESS_COMPLETED == progress || DfuConstant.PROGRESS_ABORTED == progress))
            return;
        mLastNotificationTime = now;

        Message message = Message.obtain();
        Bundle bundle = new Bundle();
        bundle.putInt(DfuConstant.EXTRA_DATA, info.getProgress());
        bundle.putFloat(DfuConstant.EXTRA_SPEED_B_PER_MS, info.getSpeed());
        bundle.putFloat(DfuConstant.EXTRA_AVG_SPEED_B_PER_MS, info.getAverageSpeed());
        message.setData(bundle);
        message.what = DfuMessage.DFU_PROGRESS_INFO_MESSAGE.getValue();

        if (getListener() != null) {
            getListener().onMessage(getBluetoothDevice(), message);
        }
    }

    public void startUpgrade() {
        dfuThread = new DfuThread();
        dfuThread.start();
    }

    public void resume() {
        notifyLock();
    }

    public void abort() {
        mAborted = true;
        notifyLock();
    }
}
